using System;
using System.Collections.Generic;
using System.Text;
using System.Xml;
using WMM.Tapioca.Taxonomy;
using WMM.Tapioca.Instance;
using System.Xml.Linq;
using System.Linq;
using WMM.Tapioca.Utilities;
using System.Xml.Schema;

namespace WMM.Tapioca.Instance
{
    /// <summary>
    /// XBRL 2.1 Specification 4.7
    /// The context element contains information about the entity being described, the reporting period and the reporting scenario,
    /// all of which are necessary for understanding a business fact captured as an XBRL item.
    /// </summary>
    public class Context
    {
        private static readonly log4net.ILog log = log4net.LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType);

        private XElement contextElement;

        /// <returns> Context XElement. </returns>
        public XElement ContextElement
        {
            get { return contextElement; }
            set { contextElement = value; }
        }

        private XElement instantElement;

        private bool isDurationPeriod;

        private XElement startDateElement;

        /// <summary>
        /// The this end date duration node
        /// </summary>
        private XElement endDateElement;

        private DTS dts;

        private XElement periodElement;

        /// <returns> Period node of this context. </returns>
        public XElement PeriodElement
        {
            get { return periodElement; }
            set { periodElement = value; }
        }

        private string id;

        /// <summary>
        /// The ID of this context.
        /// </summary>
        public string Id
        {
            get { return id; }
            set { id = value; }
        }

        private bool isInstantPeriod;

		/// <summary>
		/// Describes whether or not this context uses an instant period. Returns true if this context uses an instant
		/// period. Returns false is this context does not use an instant period.
		/// </summary>
		public bool IsInstantPeriod
		{
            get { return isInstantPeriod; }
            set { isInstantPeriod = value; }
		}

		/// <summary>
		/// Describes whether or not this context uses a duration period. Returns true if this context uses a duration
		/// period. Returns false is this context does not use a duration period.
		/// </summary>
		public bool IsDurationPeriod
        {
            get
            {
                if ((isForeverPeriod) || (isDurationPeriod))
                {
                    return true;
                }

                return false;
            }
        }

        private bool isForeverPeriod;

		/// <summary>
		/// Describes whether or not this context uses a forever period. Returns true if this context uses a forever
		/// period. Returns false is this context does not use a forever period.
		/// </summary>
		public bool IsForeverPeriod
		{
            get { return isForeverPeriod; }
            set { isForeverPeriod = value; }
		}
        
        private XElement entityElement;

        /// <summary>
        /// The entity element documents the entity (business, government department, individual, etc.) that fact describes.
        /// </summary>
        public XElement EntityElement
        {
            get { return entityElement; }
            set { entityElement = value; }
        }

        private string identifier;

        /// <summary>
        /// The identifier for this entity.
        /// </summary>
        public string Identifier
        {
            get { return identifier; }
            set { identifier = value; }
        }

        private string identifierScheme;

        /// <summary>
        /// An identifier element specifies a scheme for identifying business entities..
        /// </summary>
        public string IdentifierScheme
        {
            get { return identifierScheme; }
            set { identifierScheme = value; }
        }

        private XElement segmentElement;

		/// <summary>
        /// The segment element is an optional container for additional mark-up that the preparer of an XBRL instance 
        /// SHOULD use to identify the business segment more completely in cases where the entity identifier is insufficient.
		/// </summary>
		public XElement SegmentElement
		{
            get { return segmentElement; }
            set { segmentElement = value; }
		}

        private XElement scenarioElement;

		/// <summary>
        /// Business facts can be reported as actual, budgeted, restated, pro forma, etc. For internal reporting purposes, 
        /// there can be an even greater variety of additional metadata that preparers want to associate with items. 
        /// The optional scenario element allows additional valid mark-up (see note above regarding segment)
        /// to be included for this purpose.
		/// </summary>
		public XElement ScenarioElement
		{
            get { return scenarioElement; }
            set { scenarioElement = value; }
		}

        /// <summary>
        /// A list of explicit member dimension.
        /// </summary>
        public List<XElement> ExplicitMembers
        {
            get;
            private set;
        }

        /// <summary>
        /// A list of typed member dimension.
        /// </summary>
        public List<XElement> TypedMembers
        {
            get;
            private set;
        }

        private InstanceDocument instance;

		/// <summary>
		/// A reference to the <see cref="InstanceDocument"/> in which this context is found.
		/// </summary>
		public InstanceDocument Instance
		{
            get { return instance; }
            set { instance = value; }
		}

        private DateTime periodStartDate;

		/// <summary>
		/// The start date of the period of this context. 
		/// </summary>		
		public DateTime PeriodStartDate
		{
            get { return periodStartDate; }
            set { periodStartDate = value; }
		}

        private DateTime periodEndDate;

		/// <summary>
		/// The end date of the period of this context. 
		/// </summary>		
		public DateTime PeriodEndDate
		{
            get { return periodEndDate; }
            set { periodEndDate = value; }
		}

        private DateTime instantDate;

		/// <summary>
		/// The date of the instant of this context. 
		/// </summary>		
		public DateTime InstantDate
		{
            get { return instantDate; }
            set { instantDate = value; }
		}


        internal Context(InstanceDocument instanceDocument, XElement context, DTS dtsInstance)
        {
            ExplicitMembers = new List<XElement>();
            TypedMembers = new List<XElement>();

            dts = dtsInstance;
            instance = instanceDocument;

            contextElement = context;
            id = contextElement.Attribute("id").Value;

            periodStartDate = DateTime.MinValue;
            periodEndDate = DateTime.MinValue;

            foreach (XElement currentElement in contextElement.Elements())
            {                
                if (currentElement.Name.LocalName.Equals("entity"))
                {
                    entityElement = currentElement;
                    ProcessEntity(currentElement);
                }
                else if (currentElement.Name.LocalName.Equals("period"))
                {
                    periodElement = currentElement;
                    ProcessPeriod(currentElement);
                    ValidatePeriod();
                }
                else if (currentElement.Name.LocalName.Equals("scenario"))
                {
                    scenarioElement = currentElement;
                    ProcessScenario(currentElement);
                }
            }
        }


        private void ProcessEntity(XElement entityNode)
        {
            identifier = string.Empty;
            identifierScheme = string.Empty;

            foreach (XElement currentElement in entityNode.Elements())
            {
                if (currentElement.Name.LocalName.Equals("identifier"))
                {
                    ProcessIdentifier(currentElement);
                }
                else if (currentElement.Name.LocalName.Equals("segment"))
                {
                    ProcessSegment(currentElement);
                }
            }
        }


        private void ProcessScenario(XElement scenarioNode)
        {
            this.ScenarioElement = scenarioNode;

            foreach (XElement currentElement in scenarioNode.Elements())
            {
                if (currentElement.Name.LocalName.Equals("explicitMember"))
                {
                    ExplicitMembers.Add(currentElement);
                }

                else if (currentElement.Name.LocalName.Equals("typedMember"))
                {
                    TypedMembers.Add(currentElement);
                }

            }

            ValidateScenario();
        }


        private void ValidateScenario()
        {
            foreach (XElement currentElement in this.ScenarioElement.Elements())
            {
                ValidateScenarioNode(currentElement);
            }
        }


        private void ValidateScenarioNode(XElement scenarioNode)
        {
            if (scenarioNode.HasNamespace("http://www.xbrl.org/2003/instance"))
            {
                string messageFormat = AssemblyResources.GetName("ScenarioNodeUsingXBRLNamespace");
                StringBuilder messageBuilder = new StringBuilder();
                messageBuilder.AppendFormat(messageFormat, this.Id, scenarioNode.Name);
                log.Error("Validation error on scenario", new XbrlException(messageBuilder.ToString()));
                dts.ValidatedInstance = false;
            }
            if (scenarioNode.Name.NamespaceName.Length > 0)
            {
                TaxonomySchema nodeSchema = this.Instance.GetXbrlSchemaForNamespace(scenarioNode.Name.NamespaceName);
                if (nodeSchema != null)
                {
                    Concept nodeElement = nodeSchema.GetConceptByName(scenarioNode.Name.NamespaceName, scenarioNode.Name.LocalName);
                    if (nodeElement != null)
                    {
                        if (nodeElement.SubstitutionGroup != Concept.EnumSubstitutionGroup.Unknown)
                        {
                            string messageFormat = AssemblyResources.GetName("ScenarioNodeUsingSubGroupInXBRLNamespace");
                            StringBuilder messageBuilder = new StringBuilder();
                            messageBuilder.AppendFormat(messageFormat, this.Id, scenarioNode.Name, nodeSchema.PathFile);
                            log.Error("Validation error on scenario", new XbrlException(messageBuilder.ToString()));
                            dts.ValidatedInstance = false;
                        }
                    }
                }
            }
        }


        private void ProcessSegment(XElement segmentNode)
        {
            segmentElement = segmentNode;

            foreach (XElement currentElement in segmentNode.Elements())
            {
                if (currentElement.Name.LocalName.Equals("explicitMember"))
                {
                    ExplicitMembers.Add(currentElement);
                }

                else if (currentElement.Name.LocalName.Equals("typedMember"))
                {
                    TypedMembers.Add(currentElement);
                }

            }

            ValidateSegment();
        }

        private void ValidateSegment()
        {
            foreach (XElement currentElement in this.SegmentElement.Elements())
            {
                ValidateSegmentNode(currentElement);
            }
        }


        private void ValidateSegmentNode(XElement segmentNode)
        {
            if (segmentNode.HasNamespace("http://www.xbrl.org/2003/instance"))
            {
                string messageFormat = AssemblyResources.GetName("SegmentNodeUsingXBRLNamespace");
                StringBuilder messageBuilder = new StringBuilder();
                messageBuilder.AppendFormat(messageFormat, this.Id, segmentNode.Name);
                log.Error("Validation error on segment", new XbrlException(messageBuilder.ToString()));
                dts.ValidatedInstance = false;
            }

            if (segmentNode.Name.NamespaceName.Length > 0)
            {
                TaxonomySchema nodeSchema = this.Instance.GetXbrlSchemaForNamespace(segmentNode.Name.NamespaceName);
                if (nodeSchema != null)
                {
                    Concept nodeElement = nodeSchema.GetConceptByName(segmentNode.Name.NamespaceName, segmentNode.Name.LocalName);
                    if (nodeElement != null)
                    {
                        if (nodeElement.SubstitutionGroup != Concept.EnumSubstitutionGroup.Unknown)
                        {
                            string messageFormat = AssemblyResources.GetName("SegmentNodeUsingSubGroupInXBRLNamespace");
                            StringBuilder messageBuilder = new StringBuilder();
                            messageBuilder.AppendFormat(messageFormat, this.Id, segmentNode.Name, nodeSchema.PathFile);
                            log.Error("Validation error on segment", new XbrlException(messageBuilder.ToString()));
                            dts.ValidatedInstance = false;
                        }
                    }
                }
            }
        }


        private void ProcessIdentifier(XElement identifierNode)
        {
            identifier = identifierNode.Value;

            if (identifierNode.Attribute("scheme").Value != null)
            {
                identifierScheme = identifierNode.Attribute("scheme").Value;
            }
        }

        private void ProcessPeriod(XElement periodNode)
        {
            isInstantPeriod = false;
            isForeverPeriod = false;
            isDurationPeriod = false;

            foreach (XElement currentElement in periodNode.Elements())
            {
                if (currentElement.Name.LocalName.Equals("instant"))
                {
                    isInstantPeriod = true;
                    instantElement = currentElement;

					DateTime parsedInstantDate = DateTime.MinValue;
					DateTime.TryParse(instantElement.Value, out parsedInstantDate);
					this.InstantDate = parsedInstantDate;
                }
                else if (currentElement.Name.LocalName.Equals("forever"))
                {
                    isForeverPeriod = true;
                }
                else if (currentElement.Name.LocalName.Equals("startDate"))
                {
                    isDurationPeriod = true;
                    startDateElement = currentElement;

                    DateTime parsedStartDate = DateTime.MinValue;
                    DateTime.TryParse(startDateElement.Value, out parsedStartDate);
                    this.PeriodStartDate = parsedStartDate;
                }
                else if (currentElement.Name.LocalName.Equals("endDate"))
                {
                    endDateElement = currentElement;

                    DateTime parsedEndDate = DateTime.MinValue;
                    DateTime.TryParse(endDateElement.Value, out parsedEndDate);
                    this.PeriodEndDate = parsedEndDate;
                }
            }
        }

        private void ValidatePeriod()
        {
			if ((periodStartDate != DateTime.MinValue) && (periodEndDate != DateTime.MinValue))
            {
				if (this.PeriodEndDate < this.PeriodStartDate)
                {
                    string messageFormat = AssemblyResources.GetName("PeriodEndDateLessThanPeriodStartDate");
                    StringBuilder messageBuilder = new StringBuilder();
                    messageBuilder.AppendFormat(messageFormat, this.Id);
                    log.Error("Validation error on period", new XbrlException(messageBuilder.ToString()));
                    dts.ValidatedInstance = false;
                }
            }
        }


        /// <summary>
        /// XBRL 2.1 Specification 4.10
        /// Returns true if this context is Structure Equal (s-equal) to a supplied context,
        /// and false otherwise. 
        /// </summary>
        internal bool StructureEquals(Context otherContext)
        {
            // Normalize this thisContextElement

            contextElement.DescendantNodes().OfType<XComment>().Remove();
           
            otherContext.contextElement.DescendantNodes().OfType<XComment>().Remove();            

            if (contextElement == null || otherContext.contextElement == null)
            {
                return false;
            }

            if (HaveDiferentStructure(contextElement, otherContext.contextElement))
            {
                return false;
            }

            return true;
        }


        private bool HaveDiferentStructure(XElement elementA, XElement elementB)
        {
            if (elementA.Descendants().Count() != elementB.Descendants().Count())
            {
                return true;
            }

            for (var i = 0; i < elementA.Elements().Count(); i++)
            {
                var nodeA = elementA.Elements().ElementAt(i);
                var nodeB = elementB.Elements().ElementAt(i);

                if (nodeA.Name != nodeB.Name)
                {
                    return true;
                }

                if ((nodeA.HasElements && !nodeB.HasElements) ||
                    (!nodeB.HasElements && nodeA.HasElements))
                {
                    return true;
                }

                if (!nodeA.HasElements && !nodeB.HasElements)
                {
                    // compare element value

                    if (nodeA.IsNumeric())
                    {
                        if (!nodeA.CompareElementValue(nodeB))
                        {
                            return true;
                        }
                    }
                    else
                    {
                        if (nodeA.GetSchemaInfo().SchemaType != null &&
                            nodeA.GetSchemaInfo().SchemaType.TypeCode == XmlTypeCode.Boolean)
                        {
                            if (!NormalizeBooleanValue(nodeA).Equals(NormalizeBooleanValue(nodeB)))
                            {
                                return true;
                            }
                        }
                        else
                        {
                            if (!nodeA.Value.Equals(nodeB.Value))
                            {
                                return true;
                            }

                        }
                    }
                }

                // Compare attributes

                if (nodeA.Attributes() == null)
                {
                    if (nodeB.Attributes() != null)
                    {
                        return true;
                    }
                    else
                    {
                        continue;
                    }
                }

                if (nodeA.Attributes().Count() != nodeB.Attributes().Count())
                {
                    return true;
                }


                IEnumerable<XAttribute> attributesA = Normalization.NormalizeAttributes(nodeA, true);
                IEnumerable<XAttribute> attributesB = Normalization.NormalizeAttributes(nodeB, true);

                for (var j = 0; j < attributesA.Count(); j++)
                {
                    var attrA = attributesA.ElementAt(j);
                    var attrB = attributesB.ElementAt(j);

                    if (attrA.Name != attrB.Name)
                    {
                        return true;
                    }

                    if (attrA.Value.Equals("NaN"))
                    {
                        return true;
                    }

                    if (attrA.Value != attrB.Value)
                    {
                        return true;
                    }
                }

                // recursive child elements

                if (nodeA.HasElements && nodeB.HasElements)
                {
                    if (HaveDiferentStructure(nodeA, nodeB))
                    {
                        return true;
                    }
                }
                else
                {
                    continue;
                }
            }

            return false;
        }


        private string NormalizeBooleanValue(XElement node)
        {
            string currAttrValue = node.Value;

            if (node.Value.Equals("1"))
            {
                currAttrValue = "true";
            }
            if (node.Value.Equals("0"))
            {
                currAttrValue = "false";
            }

            return currAttrValue;
        }
       

        /// <summary>
        /// Compares the period type of this context with the period type of another context.
        /// </summary>
        /// <param name="otherContext">
        /// The other context with which types should be compared.
        /// </param>
        /// <returns>
        /// True if the two contexts have the same period type; false otherwise.
        /// </returns>
        internal bool PeriodElementEquals(Context otherContext)
        {
            if (XNode.DeepEquals(periodElement, otherContext.periodElement))
            {
                return true;
            }
           
            return false;
        }
        
    }
}
